package com.firemoss.atonement.mapper
{
	import com.firemoss.atonement.entitymanager.EntityManager;
	import com.firemoss.atonement.mapper.PropertyResolution;
	import com.firemoss.atonement.mapper.error.InvalidMappingsError;
	import com.firemoss.atonement.mapper.error.UnresolvedNameMapping;
	import com.firemoss.atonement.mapper.error.UnresolvedTypeMapping;
	import com.firemoss.atonement.mapper.resolution.DefaultValueResolverFactory;
	import com.firemoss.atonement.mapper.resolution.IValueResolverFactory;
	import com.firemoss.atonement.metadata.DefaultMetadataBuilder;
	import com.firemoss.atonement.metadata.IMetadataBuilder;
	import flash.utils.Dictionary;
	import org.as3commons.lang.ClassUtils;
	import org.as3commons.reflect.Type;

	/**
	 * Provides support for object:object property mapping and building
	 * maps to be used to do perform the mapping.
	 */
	public class Mapper
	{

		//----------------------------------------------------------
		// Public Properties 
		//----------------------------------------------------------

		/**
		 * Should the case of property names match when performing mapping?
		 */
		public var caseSensitivePropertyNames : Boolean = true

		/**
		 * The entity manager that can be used to resolve mapping targets.
		 */
		public var entityManager : EntityManager

		/**
		 * The IGraphedInstanceFactory to use when mapping operations need to create new
		 * instances of a given class encountered while performing a mapping operation.
		 */
		public var graphedInstanceFactory : IGraphedInstanceFactory = new DefaultGraphedInstanceFactory()

		/**
		 * Can be used to set mappings, all at once, as an array.  Useful when configuring
		 * via an IoC container such as the one provided by the Swiz framework.
		 */
		public function set mappings( mappings : Array ) : void
		{
			for each ( var mapping : Mapping in mappings )
			{
				addMapping( mapping )
			}
		}

		/**
		 * The IMetadataBuilder to use to build metadata about mapped classes.  Defaults
		 * to the DefaultMetadataBuilder.
		 */
		public var metadataBuilder : IMetadataBuilder = new DefaultMetadataBuilder()

		/**
		 * The IValueResolverFactory to use when creating value resolvers for mapped properties.
		 * Defaults to the DefaultValueResolverFactory.
		 */
		public var valueResolverFactory : IValueResolverFactory = new DefaultValueResolverFactory()

		//----------------------------------------------------------
		// Private Properties 
		//----------------------------------------------------------

		/**
		 * Lookup table of maps:  keyed by [source][target]
		 */
		private var mapTable : Dictionary;

		//----------------------------------------------------------
		//   Constructor 
		//----------------------------------------------------------

		public function Mapper()
		{
			resetMapTable();
		}

		//----------------------------------------------------------
		// Public Methods 
		//----------------------------------------------------------

		/**
		 * Creates a new Mapping for the given source and target and return the Mapper itself
		 */
		public function addMapping( mapping : Mapping ) : Mapper
		{
			mapping.metadataBuilder = this.metadataBuilder
			mapping.mapper = this

			if ( !mapping.source || !mapping.target )
			{
				throw new Error( "Can't add a mapping with a null source or target!" );
			}

			if ( !mapTable[ mapping.source ])
			{
				mapTable[ mapping.source ] = new Dictionary()
			}

			mapTable[ mapping.source ][ mapping.target ] = mapping

			mapping.resolveProperties( valueResolverFactory );

			return this
		}

		/**
		 * Attempts to validate all mappings.  If an invalid mapping is found,
		 * its errors are printed to the console and assertion stops.  Originally,
		 * all errors from all mappings were printed, but the output was just overwhelming,
		 * even in small test cases.
		 */
		public function assertMappingsAreValid() : void
		{
			var name : Array = []
			var type : Array = []
			var err : InvalidMappingsError

			for ( var source : * in mapTable )
			{
				for ( var target : * in mapTable[ source ])
				{
					// Go ahead and stop on the first 
					Mapping( mapTable[ source ][ target ]).assertMappingIsValid()
				}
			}

		}

		/**
		 * Clones the source object, assuming a zero-argument constructor.
		 */
		public function clone( source : Object ) : Object
		{
			var targetClass : Class = ClassUtils.forInstance( source )
			var target : Object = new targetClass()

			return map( source, target )
		}

		/**
		 * Gets a mapping between two classes
		 */
		public function getMappingFor( source : Class, target : Class ) : Mapping
		{
			if ( !mappingExistsBetween( source, target ))
			{
				throw new Error( "You've asked for a mapping between " + Type.forClass( source ).name + " and " + Type.forClass( source ).name + ", but none exists." );
			}

			return mapTable[ source ][ target ];
		}

		public function map( source : Object, target : Object, mappingChain : Dictionary=null ) : Object
		{
			var sourceClass : Class = ClassUtils.forInstance( source );
			var targetClass : Class = ClassUtils.forInstance( target );

			if ( !mappingExistsBetween( sourceClass, targetClass ))
			{
				createImplicitMapping( sourceClass, targetClass )
			}

			if ( mappingChain == null )
			{
				mappingChain = new Dictionary()
			}

			var mapping : Mapping = getMappingFor( sourceClass, targetClass )

			if ( !mapping.validated )
			{
				mapping.assertMappingIsValid()
			}

			for each ( var resolver : PropertyResolution in mapping.resolvedPropertyMap )
			{
				var sourceValue : *

				if ( resolver.sourceProperty && resolver.sourceProperty.readable )
				{
					sourceValue = source[ resolver.sourceProperty.name ]
				}
				else
				{
					sourceValue = source
				}

				if ( resolver.targetProperty.writeable )
				{
					target[ resolver.targetProperty.name ] = resolver.valueResolutionFunction.apply( null, [ sourceValue, resolver.targetProperty, mapping, mappingChain ])
				}
			}

			return target;
		}

		/**
		 * States if a mapping exists between two classes
		 */
		public function mappingExistsBetween( source : Class, target : Class ) : Boolean
		{
			return mapTable[ source ] && mapTable[ source ][ target ];
		}

		/**
		 * Destroys all established maps.
		 */
		public function resetMapTable() : void
		{
			mapTable = new Dictionary();
		}

		//----------------------------------------------------------
		// Private Methods 
		//----------------------------------------------------------

		private function createImplicitMapping( sourceClass : Class, targetClass : Class ) : void
		{
			var mapping : Mapping = new Mapping( sourceClass, targetClass );

			addMapping( mapping )

			var ex : InvalidMappingsError

			try
			{
				mapping.assertMappingIsValid()
			}
			catch ( e : InvalidMappingsError )
			{
				ex = e
			}

			if ( ex )
			{
				for each ( var invalidByType : UnresolvedTypeMapping in ex.unresolvedPropertiesByType )
				{
					createImplicitMapping( invalidByType.sourceProperty.type.clazz, invalidByType.targetProperty.type.clazz )
				}
			}
		}
	}
}
